package asteroids.model;

import java.util.HashSet;
import java.util.Set;

import be.kuleuven.cs.som.annotate.*;
import asteroids.ModelException;
import asteroids.model.programs.Program;
import asteroids.util.Vector;

/**
 * All aspects related to the direction are worked out nominally.
 * 
 * @author Michiel De Cuyper & Lennert De Graef
 * 
 * @invar The direction of a ship lies between 0 and 2*Pi at all times |
 *        isValidDirection(this.getDirection())
 * 
 */
public class Ship extends SpaceObject {

	//TODO Program linken en runnen

	public Ship(double radius, double mass, Vector velocity, Vector position, World world, double direction) throws ModelException {
		super(radius, mass, velocity, position, world);
		assert isValidDirection(direction);
		setDirection(direction);
	}

	/**
	 * Returns the direction of a SpaceObject
	 * 
	 * @return The direction of a SpaceObject | return this.direction
	 */
	@Basic
	@Immutable
	public double getDirection() {
		return this.direction;
	}

	/**
	 * @param direction
	 *            the direction to set (in radians)
	 * @pre The direction to set must be between 0 and 2*Pi
	 *      | 0 <= direction < 2*Math.PI
	 * @post (new this).getDirection() = direction
	 */
	@Raw
	public void setDirection(double direction) {
		assert(isValidDirection(direction));
		this.direction = direction;
	}

	/**
	 * Checks wether a given direction is valid
	 * 
	 * @return True if and only if: the direction lies between 0 and 2*Pi and it's not NaN
	 *         | return (direction >= 0) && (direction < 2 * Math.PI) && (direction != Double.NaN);
	 */
	public static boolean isValidDirection(double direction) {
		return (direction >= 0) && (direction < 2 * Math.PI) && (direction != Double.NaN);
	}

	/**
	 * A variable storing the orientation of a SpaceObject
	 */
	private double direction;

	@Override
	public boolean isValidMass(double mass) {
		return mass != Double.POSITIVE_INFINITY && mass > MINIMUM_MASS && mass != Double.NaN;
	}

	private double MINIMUM_MASS = 0;

	@Override
	public boolean isValidRadius(double radius) {
		return (radius > 0) && (radius != Double.NaN) && (radius < Double.MAX_VALUE);
	}

	/**
	 * Checks wether the thruster of a ship is active
	 * 
	 * @return True if the ship is thrusting | return this.isTrusting
	 */
	public boolean isThrusting() {
		return this.isThrusting;
	}

	/**
	 * Sets the thruster of a ship active or inactive
	 * 
	 * @param active
	 *            True if the thruster needs to be active, false if inactive
	 * 
	 * @effect if(active == true) | setAcceleration(thruster_power/this.getMass()))
	 * @effect if(active == false) | setAcceleration(0,0)
	 */
	public void setThrusterActive(boolean active) {
		this.isThrusting = active;
		if (isThrusting) {
			this.setAccelerationX((THRUSTER_POWER / this.getMass()) * Math.cos(this.getDirection()));
			this.setAccelerationY((THRUSTER_POWER / this.getMass()) * Math.sin(this.getDirection()));
		}
		else {
			this.setAccelerationX(0);
			this.setAccelerationY(0);
		}
	}

	/**
	 * A boolean variable holding information about the state of a ships thruster
	 * A double storing the power of the thruster of a ship
	 */
	private boolean isThrusting = false;
	private final double THRUSTER_POWER = 1.1 * Math.pow(10, 18);

	/**
	 * Rotates a ship by a given angle
	 * 
	 * @param angle
	 *            The amount to rotate this ship
	 * 
	 * @post ((new this).getDirection().toDegrees) % 360 == ((this.getDirection() + angle).toDegrees()) % 360
	 */
	public void rotate(double angle) {
		double degrees = Math.toDegrees(this.getDirection());
		double newAngle = Math.toRadians((degrees + Math.toDegrees(angle)) % 360);
		this.setDirection(newAngle);
	}

	/**
	 * Returns the set of fired bullets
	 * 
	 * @return ...
	 *         |this.firedBullets
	 */
	public Set<Bullet> getFiredBullets() {
		return firedBullets;
	}

	/**
	 * Adds a bullet to the fired bullets list
	 * 
	 * @param bullet
	 *            The fired bullet to be added
	 * 
	 * @post ...
	 *       |getFiredBullets().contains(bullet)
	 */
	public void addFiredBullet(Bullet bullet) {
		firedBullets.add(bullet);
	}

	/**
	 * Removes a bullet from the fired bullets list
	 * 
	 * @param bullet
	 *            The fired bullet to be removed
	 * 
	 * @post ...
	 *       |!getFiredBullets().contains(bullet)
	 */
	public void removeFiredBullet(Bullet bullet) {
		try {
			firedBullets.remove(bullet);
		}
		catch (NullPointerException e) {
			return;
		}
	}

	/**
	 * Set of all the fired bullets from this ship in the world (not dead)
	 */
	private Set<Bullet> firedBullets = new HashSet<Bullet>();

	/**
	 * Fires a bullet coming from this ship
	 * 
	 * @effect new Bullet(3, bulletVelocity, bulletPosition, this.getWorld(), this)
	 */
	public void fireBullet() {
		try {
			//Try firing a bullet. When a bullet is too close to the wall, it will fail
			Vector bulletVelocity = new Vector(250 * Math.cos(this.getDirection()), 250 * Math.sin(this.getDirection()));
			Vector bulletPosition = new Vector(this.getPositionX() + (this.getRadius() + Bullet.getBulletRadius()) * Math.cos(this.getDirection()), this.getPositionY() + (this.getRadius() + Bullet.getBulletRadius()) * Math.sin(this.getDirection()));
			Bullet newBullet = new Bullet(3, bulletVelocity, bulletPosition, this.getWorld(), this);
			this.getWorld().addBullet(newBullet);
		}
		catch (ModelException e) {
			return;
		}
	}

	/**
	 * Sets the program of the ship to the given program
	 * 
	 * @param program
	 *            Program to be set
	 * 
	 */
	public void setProgram(Program masterProgram) {
		Program program = new Program(masterProgram, this);
		this.program = program;
		isAI = true;
	}

	/**
	 * Runs the program of this ship a given amount of times
	 * 
	 * @param amountOfRuns
	 */
	public void runProgram(int amountOfRuns) {
		program.runProgram(amountOfRuns);
	}

	private Program program;

	/**
	 * Returns whether the ship is AI controlled or not
	 * 
	 * @return
	 */
	public boolean isAI() {
		return isAI;
	}

	private boolean isAI;

}
